%{

#include "callother.h"
#include "compiler_unit.h"

#define ERR_BUF_SIZE    255
#define FUNCTION_CONTEXT    0x01

static int g_current_type;
static int g_context = 0;

int yylex(void);

static void yyerror(char *s)
{
    Compiler_Error(s);
}

%}

/*
 * Token definitions.
 */
%token L_STRING
%token L_VOID_TYPE L_STRING_TYPE
%token L_IDENTIFIER

%token L_RETURN

/*
 * Operator precedence and associativity.
 */
%right '='
%left '+'

/*
 * YYTYPE
 */
%union
{
    int type;
    int num_arg;
    CoValue *value;
    AstNode *node;
	struct {
		AstNode *node;
		int num;
	} decl;
    int number;
}

/*
 * Token types.
 */

/* type */
%type <type> return_type basic_type
%type <type> L_VOID_TYPE L_STRING_TYPE
%type <type> new_arg

/* number of arguments */
%type <num_arg> argument argument_list

/* CoString */
%type <value> L_STRING
%type <value> L_IDENTIFIER
%type <value> new_variable_name

/* AST Node */
%type <node> all program chunk
%type <node> block_or_semi
%type <node> statements statement
%type <node> lvalue
%type <node> new_variable_def
%type <node> expr_list expr_list2 expr_list_node
%type <node> expr0 expr4 string
%type <node> return

/* information about blocks and variables */
%type <decl> block local_declarations new_variable_list decl_block

%%

all:
		program
{
	debug(YACC, "all : program");

    Compiler_SetAstTree($$);
}
    ;

program:
        chunk program
{
	debug(YACC, "program : chunk program");

    if ($1 && $2) {
        $$ = AstNode_CreateTwoValues(0, $1, $2);
    }
    else {
        $$ = ($1 ? $1 : $2);
    }
}
	|	/* empty */
{
	debug(YACC, "program : ");

    $$ = 0;
}
	;

chunk:
		return_type L_IDENTIFIER
{
	debug(YACC, "chunk : return_type L_IDENTIFIER ...");

    g_context |= FUNCTION_CONTEXT;
}
		'(' argument ')'
{
    CoValue *scope;

    debug(YACC, "chunk : ... ( argument ) ...");

    scope = Compiler_GetIdentifierScope($2);
    if (scope == NULL) {
        Compiler_DeclareNewFunction($2, $1, $5);
    }
    else if (scope == g_scope_function) {
        Compiler_CheckFunctionDeclaration($2, $1, $5);
    }
    else {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Illegal function declaration: '%s' is already a %s",
                CoString_AS_STRING($2),
                CoString_AS_STRING(scope));
        yyerror(buf);
    }
}
		block_or_semi
{
    CoValue *scope;

	debug(YACC, ("chunk : ... block_or_semi"));

    scope = Compiler_GetIdentifierScope($2);
    if (scope == g_scope_function && $8) {
        Compiler_DefineFunction($2, $8);
    }

    $$ = 0;

    CO_DECREF($2);
    Compiler_FreeAllLocals();

    g_context &= ~FUNCTION_CONTEXT;
}
    |   return_type new_variable_list ';'
{
	debug(YACC, ("chunk : return_type new_variable_list ;"));

    if ($1 == CTYPE_VOID) {
        /* TODO: void type on variables */
    }

    $$ = $2.node;
}
    |   statement
{
	debug(YACC, ("chunk : statement"));

    $$ = $1;
}
	|	error ';'
{
	debug(YACC, ("chunk : error ;"));

    $$ = 0;
}
	;

return_type:
        L_VOID_TYPE
{
	debug(YACC, ("return_type : L_VOID_TYPE"));

    /* for variables */
    g_current_type = CTYPE_ANY;

    /* for functions */
    $$ = CTYPE_VOID;
}
    |   basic_type
{
	debug(YACC, ("return_type : basic_type"));

    /* for variables */
    g_current_type = $1;

    /* for functions */
    $$ = $1;
}
    ;

basic_type:
        L_STRING_TYPE
{
	debug(YACC, ("basic_type : L_STRING_TYPE"));
    $$ = CTYPE_STRING;
}
    ;

argument:
		/* empty */
{
	debug(YACC, ("argment : "));

    $$ = 0;
}
	|	argument_list
{
	debug(YACC, ("argment : argument_list"));

    $$ = $1;
}
	;

argument_list:
		new_arg
{
	debug(YACC, ("argument_list : new_arg"));

    $$ = 1;
}
	|	argument_list ',' new_arg
{
	debug(YACC, ("argument_list : argument_list , new_arg"));

    $$ = $1 + 1;
}
	;

new_arg:
		basic_type
{
	debug(YACC, ("new_arg : basic_type"));

    $$ = $1;
}
	|	basic_type new_variable_name
{
    debug(YACC, ("new_arg : basic_type new_variable_name"));

    $$ = $1;

    Compiler_AddLocal($2, $1);
    CO_DECREF($2);
}
	;

new_variable_name:
        L_IDENTIFIER
{
    CoValue *scope;

    debug(YACC, ("new_variable_name : L_IDENTIFER"));

    scope = Compiler_GetIdentifierScope($1);
    if (scope != NULL) {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Illegal variable declaration: '%s' is already a %s",
                CoString_AS_STRING($1),
                CoString_AS_STRING(scope));
        yyerror(buf);
    }

    $$ = $1;
}
    ;

block_or_semi:
		block
{
	debug(YACC, ("block_or_semi : block"));

    $$ = $1.node;
    if (!$$) {
        $$ = AstNode_CreateReturn(0);
    }
}
	|	';'
{
	debug(YACC, ("block_or_semi : ;"));

    $$ = 0;
}
	|	error
{
	debug(YACC, ("block_or_semi : error"));

    $$ = 0;
}
	;

block:
		'{' local_declarations statements '}'
{
	debug(YACC, ("block : { local_declarations statements }"));

    if ($2.node && $3) {
        $$.node = AstNode_CreateTwoValues(0, $2.node, $3);
    }
    else {
        $$.node = ($2.node ? $2.node : $3);
    }
    $$.num = $2.num;
}
	;

local_declarations:
		/* empty */
{
	debug(YACC, ("local_declarations : "));

    $$.node = 0;
    $$.num = 0;
}
	|	local_declarations basic_type
{
	debug(YACC, ("local_declarations : local_declarations basic_type ..."));

    g_current_type = $2;
}
		new_variable_list ';'
{
	debug(YACC, ("local_declarations : ... new_variable_list ;"));

    if ($1.node && $4.node) {
        $$.node = AstNode_CreateTwoValues(0, $1.node, $4.node);
    }
    else {
        $$.node = ($1.node ? $1.node : $4.node);
    }
    $$.num = $1.num + $4.num;
}
	;

new_variable_list:
		new_variable_def
{
	debug(YACC, ("new_variable_list : new_variable_def"));

    $$.node = $1;
    $$.num = 1;
}
	|	new_variable_def ',' new_variable_list
{
	debug(YACC, ("new_variable_list : new_variable_def , new_variable_list"));

    $$.node = AstNode_CreateTwoValues(0, $1, $3.node);
    $$.num = 1 + $3.num;
}
	;

new_variable_def:
		new_variable_name
{
	debug(YACC, ("new_variable_def : new_variable_name"));

    if (g_context & FUNCTION_CONTEXT) {
        /* TODO: LOCAL_MOD_UNUSED */
        Compiler_AddLocal($1, g_current_type);
    }
    else
    {
        Compiler_AddGlobal($1, g_current_type);
    }

    $$ = 0;
    CO_DECREF($1);
}
	|	new_variable_name '=' expr0
{
    int index;
    AstNode *right;

	debug(YACC, ("new_variable_def : new_variable_name = expr0"));

    if (g_context & FUNCTION_CONTEXT) {
        /* TODO: LOCAL_MOD_UNUSED */
        index = Compiler_AddLocal($1, g_current_type);

        /* TODO: do promotions */
        $$ = AstNode_CreateUnaryOpWithOneArg(F_VOID_ASSIGN_LOCAL, 0, $3, index);
    }
    else
    {
        index = Compiler_AddGlobal($1, g_current_type);

        right = AstNode_CreateOpCodeWithOneArg(F_GLOBAL_LVALUE, 0, index);
        $$ = AstNode_CreateBinaryOp(F_VOID_ASSIGN, 0, $3, right);
    }

    CO_DECREF($1);
}
	;

expr0:
        lvalue '=' expr0
{
	debug(YACC, ("expr0 : lvalue = expr0"));

    $$ = AstNode_CreateBinaryOp(F_ASSIGN, $3->type, $3, $1);

    /* TODO: check type, do promotions */
}
    |   error '=' expr0
{
	debug(YACC, ("expr0 : error = expr0"));

    yyerror("Illegal LHS");

    $$ = AstNode_CreateError();
}
    |   expr0 '+' expr0
{
    int result_type;
    int t1;
    int t3;

	debug(YACC, ("expr0 : expr0 + expr0"));

    t1 = $1->type;
    t3 = $3->type;

    /* TODO: check type more */
    if (t1 == t3) {
        result_type = t1;
    }
    else if (t1 == CTYPE_ANY) {
        result_type = t3;
    }
    else if (t3 == CTYPE_ANY) {
        result_type = t1;
    }
    else {
        result_type = CTYPE_ANY;
    }

    $$ = AstNode_CreateBinaryOp(F_ADD, result_type, $1, $3);
}
	|	expr4
{
	debug(YACC, ("expr0 : expr4"));

    $$ = $1;
}
    ;

lvalue:
        expr4
{
	debug(YACC, ("lvalue : expr4"));

    /* Restrictive lvalues, but I think they make more sense :) */
    $$ = $1;
    switch($$->kind) {
        default:
            yyerror("Illegal lvalue");
            break;
        case NODE_OPCODE_1:
        case NODE_UNARY_OP_1:
        case NODE_BINARY_OP:
            switch ($$->v.number) { /* make it a lvalue */
                case F_LOCAL: $$->v.number = F_LOCAL_LVALUE; break;
                case F_GLOBAL: $$->v.number = F_GLOBAL_LVALUE; break;
                default: yyerror("Illegal lvalue"); break;
            }
            break;
    }
}
    ;

expr4:
        L_IDENTIFIER '(' expr_list ')'
{
    CoValue *scope;

    debug(YACC, ("expr4 : L_IDENTIFIER ( expr_list )"));

    scope = Compiler_GetIdentifierScope($1);
    if (scope == g_scope_efun) {
        $$ = Compiler_CallEFun($1, $3);
    }
    else if (scope == g_scope_function) {
        $$ = Compiler_CallFunction($1, $3);
    }
    else if (scope != NULL) {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "%s name '%s' is not callable",
                CoString_AS_STRING(scope),
                CoString_AS_STRING($1));
        $$ = AstNode_CreateError();
        yyerror(buf);
    }
    else {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Undefined function '%s'",
                CoString_AS_STRING($1));
        $$ = AstNode_CreateError();
        yyerror(buf);
    }

    CO_DECREF($1);
}
    |   L_IDENTIFIER
{
    CoValue *scope;

	debug(YACC, ("expr4 : L_IDENTIFIER"));

    scope = Compiler_GetIdentifierScope($1);
    if (scope == g_scope_local)
    {
        $$ = Compiler_UseLocal($1);
    }
    else if (scope == g_scope_global)
    {
        $$ = Compiler_UseGlobal($1);
    }
    else if (scope != NULL) {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Illegal variable name: '%s' is a %s",
                CoString_AS_STRING($1),
                CoString_AS_STRING(scope));
        $$ = AstNode_CreateError();
        yyerror(buf);
    }
    else {
        char buf[ERR_BUF_SIZE + 2];
        snprintf(buf, ERR_BUF_SIZE, "Undefined variable '%s'",
                CoString_AS_STRING($1));
        $$ = AstNode_CreateError();
        yyerror(buf);
    }

    CO_DECREF($1);
}
	|	string
{
	debug(YACC, ("expr4 : string"));

    $$ = $1;
}
    ;

expr_list:
        /* empty */
{
	debug(YACC, ("expr_list : "));

    $$ = AstNode_CreateExprList(0);
}
    |   expr_list2
{
	debug(YACC, ("expr_list : expr_list2"));

    $$ = AstNode_CreateExprList($1);
}
    |   expr_list2 ','
{
	debug(YACC, ("expr_list : expr_list2 ,"));

    $$ = AstNode_CreateExprList($1);
}
    ;

expr_list2:
        expr_list_node
{
	debug(YACC, ("expr_list2 : expr_list_node"));

    $$ = $1;
}
    |   expr_list2 ',' expr_list_node
{
	debug(YACC, ("expr_list2 : expr_list2 , expr_list_node"));

    $$ = AstNode_ConnectExprNodes($1, $3);
}
    ;

expr_list_node:
        expr0
{
	debug(YACC, ("expr_list_node : expr0"));

    $$ = AstNode_CreateExprNode($1);
}
    ;

string:
		L_STRING
{
    int index;

	debug(YACC, ("string : L_STRING"));

    index = Compiler_AddStringConst($1);
    CO_DECREF($1);

    $$ = AstNode_CreateString(index);
}
    ;

statements:
		/* empty */
{
	debug(YACC, ("statements : "));

    $$ = 0;
}
	|	statement statements
{
	debug(YACC, ("statements : statement statements"));

    if ($1 && $2) {
        $$ = AstNode_CreateTwoValues(0, $1, $2);
    }
    else {
        $$ = ($1 ? $1 : $2);
    }
}
	|	error ';'
{
	debug(YACC, ("statements : error ;"));

    $$ = 0;
}
	;

statement:
        expr0 ';'
{
	debug(YACC, ("statement : expr0 ;"));

    $$ = AstNode_PopValue($1);
}
	|	return
{
	debug(YACC, ("statement : return"));

    if (g_context & FUNCTION_CONTEXT) {
        $$ = $1;
    }
    else {
        /* TODO: return outside function, pop them */
        debug(YACC, ("[ERROR] return outside function"));
        $$ = 0;
    }
}
	|	decl_block
{
	debug(YACC, ("statement : decl_block"));

    if (g_context & FUNCTION_CONTEXT) {
        Compiler_PopLocals($1.num);
    }
    else {
        Compiler_PopGlobals($1.num);
    }
    $$ = $1.node;
}
	|	/* empty */ ';'
{
	debug(YACC, ("statement : ;"));

    $$ = 0;
}
    ;

return:
		L_RETURN ';'
{
	debug(YACC, ("return : L_RETURN ;"));

    /* TODO: check return type */

    $$ = AstNode_CreateReturn(0);
}
	|	L_RETURN expr0 ';'
{
	debug(YACC, ("return : L_RETURN expr0 ;"));

    /* TODO: check return type */

    if (AstNode_IsNode($2, NODE_NUMBER, 0)) {
        $$ = AstNode_CreateReturn(0);
    } else {
        $$ = AstNode_CreateReturn($2);
    }
}
	;

decl_block:
         block
{
    debug(YACC, ("decl_block : block"));

    $$ = $1;
}
    ;

%%
